package com.hqd.ch03.v22.aop.framework.aspectj;

import com.hqd.ch03.v22.aop.ProxyMethodInvocation;
import com.hqd.ch03.v22.aop.interceptor.ExposeInvocationInterceptor;
import com.hqd.ch03.v22.aopalliance.aop.Advice;
import com.hqd.ch03.v22.aopalliance.intercept.MethodInvocation;
import com.hqd.ch03.v22.core.DefaultParameterNameDiscoverer;
import com.hqd.ch03.v22.core.ParameterNameDiscoverer;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.weaver.tools.JoinPointMatch;
import org.aspectj.weaver.tools.PointcutParameter;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedenceInformation {
    protected static final String JOIN_POINT_KEY = JoinPoint.class.getName();
    private final Class<?>[] parameterTypes;
    private final Class<?> declaringClass;
    private String aspectName = "";
    private Method aspectJAdviceMethod;
    private Object aspect;
    private AspectJExpressionPointcut pointcut;
    /**
     * 是否存在joinpoint参数
     */
    private int joinPointArgumentIndex = -1;
    /**
     * 是否存在staticPart参数
     */
    private int joinPointStaticPartArgumentIndex = -1;
    /**
     * 返回值
     */
    private String returningName;
    /**
     * 返回值类型
     */
    private Class<?> discoveredReturningType = Object.class;
    /**
     * 异常
     */
    private String throwingName;
    /**
     * 异常类型
     */
    private Class<?> discoveredThrowingType = Object.class;
    private String[] argumentNames;
    private Map<String, Integer> argumentBindings;
    /**
     * 标记位，参数是否解析完毕
     */
    private boolean argumentsIntrospected = false;

    public AbstractAspectJAdvice(AspectJExpressionPointcut pointcut, Method aspectJAdviceMethod, Object aspect) {
        this.aspectJAdviceMethod = aspectJAdviceMethod;
        this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
        this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
        this.aspect = aspect;
        this.pointcut = pointcut;
    }

    public static JoinPoint currentJoinPoint() {
        MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation();
        ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
        JoinPoint jp = (JoinPoint) pmi.getUserAttribute(JOIN_POINT_KEY);
        if (jp == null) {
            jp = new MethodInvocationProceedingJoinPoint(pmi);
            pmi.setUserAttribute(JOIN_POINT_KEY, jp);
        }
        return jp;
    }

    protected Object invokeAdviceMethodWithGivenArgs(Object[] args) {

        Object[] actualArgs = args;
        if (this.aspectJAdviceMethod.getParameterCount() == 0) {
            actualArgs = null;
        }
        try {
            return aspectJAdviceMethod.invoke(aspect, actualArgs);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    protected Object invokeAdviceMethod(Method method, JoinPoint jp, JoinPointMatch jpMatch,
                                        Object returnValue, Throwable t, Object... args) {
        if (match(method, args)) {
            return invokeAdviceMethodWithGivenArgs(argBinding(jp, getJoinPointMatch(), returnValue, t));
        }
        return null;
    }

    protected Object invokeAdviceMethod(Method method, JoinPointMatch jpMatch,
                                        Object returnValue, Throwable t, Object... args) {
        return invokeAdviceMethod(method, getJoinPoint(), jpMatch, returnValue, t, args);
    }

    /**
     * 判断是否匹配
     *
     * @param method
     * @return
     */
    protected boolean match(Method method, Object... args) {
        return getPointcut().matches(method, this.declaringClass, args);
    }

    protected Object[] argBinding(JoinPoint jp, JoinPointMatch jpMatch,
                                  Object returnValue, Throwable ex) {
        /**
         * 计算绑定参数
         */
        calculateArgumentBindings();
        int numBound = 0;
        Object[] adviceInvocationArgs = new Object[this.parameterTypes.length];
        /**
         * 优先绑定joinpoint相关的参数
         */
        if (joinPointArgumentIndex != -1) {
            adviceInvocationArgs[0] = jp;
            numBound++;
        } else if (joinPointStaticPartArgumentIndex != -1) {
            adviceInvocationArgs[0] = jp.getStaticPart();
            numBound++;
        }
        if (MapUtils.isNotEmpty(argumentBindings)) {
            PointcutParameter[] parameterBindings = jpMatch.getParameterBindings();
            for (PointcutParameter pb : parameterBindings) {
                String name = pb.getName();
                adviceInvocationArgs[this.argumentBindings.get(name)] = pb.getBinding();
                numBound++;
            }
        }
        if (StringUtils.isNotBlank(this.returningName)) {
            adviceInvocationArgs[this.argumentBindings.get(this.returningName)] = returnValue;
            numBound++;
        }
        if (StringUtils.isNotBlank(this.throwingName)) {
            adviceInvocationArgs[this.argumentBindings.get(this.throwingName)] = ex;
            numBound++;
        }
        /**
         * 参数没有完全匹配
         */
        if (numBound != this.parameterTypes.length) {
            throw new IllegalStateException("Required to bind " + this.parameterTypes.length +
                    " arguments, but only bound " + numBound + " (JoinPointMatch " +
                    (jpMatch == null ? "was NOT" : "WAS") + " bound in invocation)");
        }
        return adviceInvocationArgs;
    }

    /**
     * 计算参数，参数列表是空，或者只有Joinpoint。则跳过参数名匹配阶段
     */
    public final void calculateArgumentBindings() {
        if (this.argumentsIntrospected || this.parameterTypes.length == 0) {
            return;
        }

        int numUnboundArgs = this.parameterTypes.length;
        Class<?>[] parameterTypes = this.aspectJAdviceMethod.getParameterTypes();
        if (maybeBindJoinPoint(parameterTypes[0]) || maybeBindProceedingJoinPoint(parameterTypes[0]) ||
                maybeBindJoinPointStaticPart(parameterTypes[0])) {
            numUnboundArgs--;
        }

        /**
         * 除了Joinpoint参数之外，还有其他参数
         */
        if (numUnboundArgs > 0) {
            bindArgumentsByName(numUnboundArgs);
        }

        this.argumentsIntrospected = true;
    }

    private void bindArgumentsByName(int numArgumentsExpectingToBind) {
        if (this.argumentNames == null) {
            this.argumentNames = createParameterNameDiscoverer().getParameterNames(this.aspectJAdviceMethod);
        }
        /**
         *参数匹配完成之后，需要放入对应的数据结构
         */
        if (this.argumentNames != null) {
            bindExplicitArguments(numArgumentsExpectingToBind);
        } else {
            throw new IllegalStateException("Advice method [" + this.aspectJAdviceMethod.getName() + "] " +
                    "requires " + numArgumentsExpectingToBind + " arguments to be bound by name, but " +
                    "the argument names were not specified and could not be discovered.");
        }
    }

    private void bindExplicitArguments(int numArgumentsLeftToBind) {
        this.argumentBindings = new HashMap<>();
        int argumentIndexOffset = this.parameterTypes.length - numArgumentsLeftToBind;
        for (int i = argumentIndexOffset; i < this.argumentNames.length; i++) {
            this.argumentBindings.put(this.argumentNames[i], i);
        }
        if (StringUtils.isNotBlank(this.returningName)) {
            Integer index = this.argumentBindings.get(this.returningName);
            this.discoveredReturningType = this.aspectJAdviceMethod.getParameterTypes()[index];
        }
        if (StringUtils.isNotBlank(this.throwingName)) {
            Integer index = this.argumentBindings.get(this.throwingName);
            this.discoveredThrowingType = this.aspectJAdviceMethod.getParameterTypes()[index];
        }
        /**
         * 接下来需要将参数设置到对应的切点中去
         */
        configurePointcutParameters(this.argumentNames, argumentIndexOffset);
    }

    private void configurePointcutParameters(String[] argumentNames, int argumentIndexOffset) {
        int numParametersToRemove = argumentIndexOffset;
        /**
         * 去除返回值异常
         */
        if (this.returningName != null) {
            numParametersToRemove++;
        }
        if (this.throwingName != null) {
            numParametersToRemove++;
        }
        String[] pointcutParameterNames = new String[argumentNames.length - numParametersToRemove];
        Class<?>[] pointcutParameterTypes = new Class<?>[pointcutParameterNames.length];
        Class<?>[] methodParameterTypes = this.aspectJAdviceMethod.getParameterTypes();

        int index = 0;
        for (int i = 0; i < argumentNames.length; i++) {
            if (i < argumentIndexOffset) {
                continue;
            }
            if (argumentNames[i].equals(this.returningName) ||
                    argumentNames[i].equals(this.throwingName)) {
                continue;
            }
            pointcutParameterNames[index] = argumentNames[i];
            pointcutParameterTypes[index] = methodParameterTypes[i];
            index++;
        }

        /**
         * 设置切点参数
         */
        this.pointcut.setPointcutParameterNames(pointcutParameterNames);
        this.pointcut.setPointcutParameterTypes(pointcutParameterTypes);
    }

    /**
     * 创建参数匹配器
     *
     * @return
     */
    protected ParameterNameDiscoverer createParameterNameDiscoverer() {
        DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
        AspectJAdviceParameterNameDiscoverer adviceParameterNameDiscoverer =
                new AspectJAdviceParameterNameDiscoverer(this.pointcut.getExpression());
        adviceParameterNameDiscoverer.setReturningName(this.returningName);
        adviceParameterNameDiscoverer.setThrowingName(this.throwingName);
        adviceParameterNameDiscoverer.setRaiseExceptions(true);
        discoverer.addDiscoverer(adviceParameterNameDiscoverer);
        return discoverer;
    }

    public AspectJExpressionPointcut getPointcut() {
        calculateArgumentBindings();
        return pointcut;
    }

    protected JoinPoint getJoinPoint() {
        return currentJoinPoint();
    }

    /**
     * 获取连接点匹配信息
     *
     * @return
     */
    protected JoinPointMatch getJoinPointMatch() {
        MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation();
        return getJoinPointMatch((ProxyMethodInvocation) mi);
    }

    protected JoinPointMatch getJoinPointMatch(ProxyMethodInvocation pmi) {
        String expression = this.pointcut.getExpression();
        return (expression != null ? (JoinPointMatch) pmi.getUserAttribute(expression) : null);
    }

    public int getOrder() {
        return 1;
    }

    private boolean maybeBindJoinPoint(Class<?> candidateParameterType) {
        if (JoinPoint.class == candidateParameterType) {
            this.joinPointArgumentIndex = 0;
            return true;
        } else {
            return false;
        }
    }

    private boolean maybeBindProceedingJoinPoint(Class<?> candidateParameterType) {
        if (ProceedingJoinPoint.class == candidateParameterType) {
            if (!supportsProceedingJoinPoint()) {
                throw new IllegalArgumentException("ProceedingJoinPoint is only supported for around advice");
            }
            this.joinPointArgumentIndex = 0;
            return true;
        } else {
            return false;
        }
    }

    protected boolean supportsProceedingJoinPoint() {
        return false;
    }

    private boolean maybeBindJoinPointStaticPart(Class<?> candidateParameterType) {
        if (JoinPoint.StaticPart.class == candidateParameterType) {
            this.joinPointStaticPartArgumentIndex = 0;
            return true;
        } else {
            return false;
        }
    }

    public Class<?> getDiscoveredReturningType() {
        return discoveredReturningType;
    }

    public Class<?> getDiscoveredThrowingType() {
        return discoveredThrowingType;
    }

    public String getReturningName() {
        return returningName;
    }

    public void setReturningName(String returningName) {
        this.returningName = returningName;
    }

    public String getThrowingName() {
        return throwingName;
    }

    public void setThrowingName(String throwingName) {
        this.throwingName = throwingName;
    }

    @Override
    public String getAspectName() {
        return aspectName;
    }

    public void setAspectName(String aspectName) {
        this.aspectName = aspectName;
    }
}
